﻿//===============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Logging Application Block
//===============================================================================
// License: http://www.codeplex.com/entlibcontrib/Project/License.aspx
// Author: Randy
//===============================================================================

using System;
using System.Text;
using System.IO;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners;

namespace EntLibContrib.Logging.TraceListeners
{
	/// <summary>
	/// Encapsulates the logic to perform rolls.
	/// </summary>
	/// <remarks>
	/// If no rolling behavior has been configured no further processing will be performed.
	/// </remarks>
	public sealed class StreamWriterRollingHelper : IDisposable
	{
		#region Fields
		/// <summary>
		/// The trace listener for which rolling is being managed.
		/// </summary>
		private NoGuidRollingFlatFileTraceListener owner;
		/// <summary>
		/// A flag indicating whether at least one rolling criteria has been configured.
		/// </summary>
		private bool performsRolling;
		/// <summary>
		/// The provider for the current date. Necessary for unit testing.
		/// </summary>
		private DateTimeProvider dateTimeProvider;
		/// <summary>
		/// A tally keeping writer used when file size rolling is configured.<para/>
		/// The original stream writer from the base trace listener will be replaced with
		/// this listener.
		/// </summary>
		private TallyKeepingFileStreamWriter managedWriter;
		/// <summary>
		/// The next date when date based rolling should occur if configured.
		/// </summary>
		private DateTime? nextRollDateTime;
		#endregion

		#region Properties
		/// <summary>
		/// Sets the date time provider.
		/// </summary>
		/// <value>The date time provider.</value>
		public DateTimeProvider DateTimeProvider
		{
			get { return this.dateTimeProvider; }
			set { this.dateTimeProvider = value; }
		}

		/// <summary>
		/// Gets the next roll date time.
		/// </summary>
		/// <value>The next roll date time.</value>
		public DateTime? NextRollDateTime
		{
			get { return this.nextRollDateTime; }
		}
		#endregion

		#region Construction
		/// <summary>
		/// Initializes a new instance of the <see cref="StreamWriterRollingHelper"/> class.
		/// </summary>
		/// <param name="owner">The owner.</param>
		public StreamWriterRollingHelper(NoGuidRollingFlatFileTraceListener owner)
		{
			this.owner = owner;
			this.dateTimeProvider = new DateTimeProvider();

			this.performsRolling = this.owner.RollInterval != RollInterval.None || this.owner.RollSizeInBytes > 0;
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Rolls if necessary.
		/// </summary>
		public void RollIfNecessary()
		{
			if (!this.performsRolling)
			{
				// avoid further processing if no rolling has been configured.
				return;
			}

			if (!UpdateRollingInformationIfNecessary())
			{
				// an error was detected while handling roll information - avoid further processing
				return;
			}

			DateTime? rollDateTime;
			if ((rollDateTime = this.CheckIsRollNecessary()) != null)
			{
				this.PerformRoll(rollDateTime.Value);
			}
		}

		/// <summary>
		/// Updates bookeeping information necessary for rolling, as required by the specified
		/// rolling configuration.
		/// </summary>
		/// <returns>
		/// true if update was successful, false if an error occurred.
		/// </returns>
		// Suppress FxCop warning message for try-catch block of all errors
		// because an unusual error in logging should not stop the application from running
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public bool UpdateRollingInformationIfNecessary()
		{
			StreamWriter currentWriter = null;

			// replace writer with the tally keeping version if necessary for size rolling
			if (this.owner.RollSizeInBytes > 0 && this.managedWriter == null)
			{
				currentWriter = owner.Writer as StreamWriter;
				if (currentWriter == null)
				{
					// TWTL couldn't acquire the writer - abort
					return false;
				}
				string actualFileName = ((FileStream)currentWriter.BaseStream).Name;

				currentWriter.Close();

				FileStream fileStream = null;
				try
				{
					fileStream = File.Open(actualFileName, FileMode.Append, FileAccess.Write, FileShare.Read);
					this.managedWriter = new TallyKeepingFileStreamWriter(fileStream, GetEncodingWithFallback());
					this.owner.Writer = this.managedWriter;
				}
				catch (Exception)
				{
					// there's a slight chance of error here - abort if this occurs and just let TWTL handle it without attempting to roll
					return false;
				}
			}

			// compute the next roll date if necessary
			if (this.owner.RollInterval != RollInterval.None && this.nextRollDateTime == null)
			{
				// Get Writer outside try in case LoggingMaxInstancesException occurs
				currentWriter = this.owner.Writer as StreamWriter;
				try
				{
					// casting should be safe at this point - only file stream writers can be the writers for the owner trace listener.
					// it should also happen rarely
					this.nextRollDateTime = CalculateNextRollDate(File.GetCreationTime(((FileStream)currentWriter.BaseStream).Name));
				}
				catch (Exception)
				{
					this.nextRollDateTime = DateTime.MaxValue;		// disable rolling if not date could be retrieved.

					// there's a slight chance of error here - abort if this occurs and just let TWTL handle it without attempting to roll
					return false;
				}
			}

			return true;
		}

		/// <summary>
		/// Checks whether rolling should be performed, and returns the date to use when performing the roll.
		/// </summary>
		/// <returns>
		/// The date roll to use if performing a roll, or <see langword="null"/> if no rolling should occur.
		/// </returns>
		/// <remarks>
		/// Defer request for the roll date until it is necessary to avoid overhead.<para/>
		/// Information used for rolling checks should be set by now.
		/// </remarks>
		public DateTime? CheckIsRollNecessary()
		{
			// check for size roll, if enabled.
			if (this.owner.RollSizeInBytes > 0 && (this.managedWriter != null && this.managedWriter.Tally > this.owner.RollSizeInBytes))
			{
				return this.dateTimeProvider.CurrentDateTime;
			}

			// check for date roll, if enabled.
			DateTime currentDateTime = this.dateTimeProvider.CurrentDateTime;
			if (this.owner.RollInterval != RollInterval.None && (this.nextRollDateTime != null && currentDateTime.CompareTo(this.nextRollDateTime.Value) >= 0))
			{
				return currentDateTime;
			}

			// no roll is necessary, return a null roll date
			return null;
		}

		/// <summary>
		/// Performs the roll.
		/// </summary>
		/// <param name="rollDateTime">The roll date time.</param>
		public void PerformRoll(DateTime rollDateTime)
		{
			string actualFileName = ((FileStream)((StreamWriter)this.owner.Writer).BaseStream).Name;

			// calculate archive name
			string archiveFileName = this.ComputeArchiveFileName(actualFileName, rollDateTime);
			// close file
			this.owner.Writer.Close();
			// move file
			try
			{
				SafeMove(actualFileName, archiveFileName, rollDateTime);
			}
			finally
			{
				// update writer - let TWTL open the file as needed to keep consistency
				this.owner.Writer = null;
				this.managedWriter = null;
				this.nextRollDateTime = null;
				this.UpdateRollingInformationIfNecessary();
			}
		}

		/// <summary>
		/// Computes the name of the archive file.
		/// </summary>
		/// <param name="actualFileName">Actual name of the file.</param>
		/// <param name="currentDateTime">The current date time.</param>
		/// <returns></returns>
		public string ComputeArchiveFileName(string actualFileName, DateTime currentDateTime)
		{
			string archiveFileName = null;

			string directory = Path.GetDirectoryName(actualFileName);
			string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(actualFileName);
			string extension = Path.GetExtension(actualFileName);

			string archiveFileNameWithTimeStampWithoutExtension = fileNameWithoutExtension;
			if (!string.IsNullOrEmpty(this.owner.TimeStampPattern))
			{
				archiveFileNameWithTimeStampWithoutExtension += "."
				+ currentDateTime.ToString(this.owner.TimeStampPattern, CultureInfo.InvariantCulture);
			}

			switch (this.owner.RollFileExistsBehavior)
			{
				case NoGuidRollFileExistsBehavior.Overwrite:
					archiveFileName = Path.Combine(directory, archiveFileNameWithTimeStampWithoutExtension + extension);
					break;
				case NoGuidRollFileExistsBehavior.Revolving:
					// look for first sequence
					int nextSequence = 1;
					DateTime nowTime = DateTime.Now;
					bool nextLoop = true;
					do
					{
						archiveFileName = Path.Combine(directory, archiveFileNameWithTimeStampWithoutExtension + "." + nextSequence + extension);
						if (!File.Exists(archiveFileName))
						{
							// does not exist
							nextLoop = false;
						}
						else
						{
							// old enough to overwrite
							DateTime fileTime = File.GetLastWriteTime(archiveFileName);
							TimeSpan dif = nowTime - fileTime;
							nextLoop = dif.TotalDays < this.owner.RevolvingDays;
							nextSequence++;
						}
					} while (nextLoop);
					break;
				case NoGuidRollFileExistsBehavior.Increment:
				default:
					// look for max sequence for date
					int maxSequence = FindMaxSequenceNumber(directory, archiveFileNameWithTimeStampWithoutExtension, extension);
					archiveFileName = Path.Combine(directory, archiveFileNameWithTimeStampWithoutExtension + "." + (maxSequence + 1) + extension);
					break;
			}

			return archiveFileName;
		}

		/// <summary>
		/// Finds the max sequence number.
		/// </summary>
		/// <param name="directoryName">Name of the directory.</param>
		/// <param name="fileName">Name of the file.</param>
		/// <param name="extension">The extension.</param>
		/// <returns></returns>
		public static int FindMaxSequenceNumber(string directoryName, string fileName, string extension)
		{
			string[] existingFiles = Directory.GetFiles(directoryName,
				string.Format(CultureInfo.CurrentCulture, "{0}*{1}", fileName, extension));

			int maxSequence = 0;
			Regex regex = new Regex(string.Format(CultureInfo.CurrentCulture, @"{0}\.(?<sequence>\d+){1}", fileName, extension));
			for (int i = 0; i < existingFiles.Length; i++)
			{
				Match sequenceMatch = regex.Match(existingFiles[i]);
				if (sequenceMatch.Success)
				{
					int currentSequence = 0;

					string sequenceInFile = sequenceMatch.Groups["sequence"].Value;
					if (!int.TryParse(sequenceInFile, out currentSequence))
						continue;		// very unlikely

					if (currentSequence > maxSequence)
					{
						maxSequence = currentSequence;
					}
				}
			}

			return maxSequence;
		}

		/// <summary>
		/// Calculates the next roll date.
		/// </summary>
		/// <param name="dateTime">The date time.</param>
		/// <returns></returns>
		public DateTime CalculateNextRollDate(DateTime dateTime)
		{
			switch (this.owner.RollInterval)
			{
				case RollInterval.Minute:
					return dateTime.AddMinutes(1);
				case RollInterval.Hour:
					return dateTime.AddHours(1);
				case RollInterval.Day:
					return dateTime.AddDays(1);
				case RollInterval.Week:
					return dateTime.AddDays(7);
				case RollInterval.Month:
					return dateTime.AddMonths(1);
				case RollInterval.Year:
					return dateTime.AddYears(1);
				default:
					return DateTime.MaxValue;
			}
		}
		#endregion

		#region Private Methods
		private static void SafeMove(string actualFileName, string archiveFileName, DateTime currentDateTime)
		{
			try
			{
				if (File.Exists(archiveFileName))
				{
					File.Delete(archiveFileName);
				}
				// take care of tunnelling issues http://support.microsoft.com/kb/172190
				File.SetCreationTime(actualFileName, currentDateTime);
				File.Move(actualFileName, archiveFileName);
			}
			catch (IOException)
			{
				// catch errors and attempt move to a new file with a GUID
				archiveFileName = archiveFileName + Guid.NewGuid().ToString();

				try
				{
					File.Move(actualFileName, archiveFileName);
				}
				catch (IOException) { }
			}
		}

		private static Encoding GetEncodingWithFallback()
		{
			Encoding encoding = (Encoding)new UTF8Encoding(false).Clone();
			encoding.EncoderFallback = EncoderFallback.ReplacementFallback;
			encoding.DecoderFallback = DecoderFallback.ReplacementFallback;
			return encoding;
		}
		#endregion

		#region IDisposable Members

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			if (managedWriter != null)
				managedWriter.Dispose();
		}

		#endregion
	}
}
